/**
 * Copyright (C) 2008 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.inject.spi;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.inject.internal.InternalFlags.getIncludeStackTraceOption;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.inject.AbstractModule;
import com.google.inject.Binder;
import com.google.inject.Binding;
import com.google.inject.Key;
import com.google.inject.MembersInjector;
import com.google.inject.Module;
import com.google.inject.PrivateBinder;
import com.google.inject.PrivateModule;
import com.google.inject.Provider;
import com.google.inject.Scope;
import com.google.inject.Stage;
import com.google.inject.TypeLiteral;
import com.google.inject.binder.AnnotatedBindingBuilder;
import com.google.inject.binder.AnnotatedConstantBindingBuilder;
import com.google.inject.binder.AnnotatedElementBuilder;
import com.google.inject.internal.AbstractBindingBuilder;
import com.google.inject.internal.BindingBuilder;
import com.google.inject.internal.ConstantBindingBuilderImpl;
import com.google.inject.internal.Errors;
import com.google.inject.internal.ExposureBuilder;
import com.google.inject.internal.InternalFlags.IncludeStackTraceOption;
import com.google.inject.internal.MoreTypes;
import com.google.inject.internal.PrivateElementsImpl;
import com.google.inject.internal.ProviderMethodsModule;
import com.google.inject.internal.util.SourceProvider;
import com.google.inject.internal.util.StackTraceElements;
import com.google.inject.matcher.Matcher;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Exposes elements of a module so they can be inspected, validated or {@link
 * Element#applyTo(Binder) rewritten}.
 *
 * @author jessewilson@google.com (Jesse Wilson)
 * @since 2.0
 */
public final class Elements {

    private static final BindingTargetVisitor<Object, Object> GET_INSTANCE_VISITOR = new DefaultBindingTargetVisitor<Object, Object>() {
        @Override
        public Object visit(InstanceBinding<?> binding) {
            return binding.getInstance();
        }

        @Override
        protected Object visitOther(Binding<?> binding) {
            throw new IllegalArgumentException();
        }
    };

    /**
     * Records the elements executed by {@code modules}.
     */
    public static List<Element> getElements(Module... modules) {
        return getElements(Stage.DEVELOPMENT, Arrays.asList(modules));
    }

    /**
     * Records the elements executed by {@code modules}.
     */
    public static List<Element> getElements(Stage stage, Module... modules) {
        return getElements(stage, Arrays.asList(modules));
    }

    /**
     * Records the elements executed by {@code modules}.
     */
    public static List<Element> getElements(Iterable<? extends Module> modules) {
        return getElements(Stage.DEVELOPMENT, modules);
    }

    /**
     * Records the elements executed by {@code modules}.
     */
    public static List<Element> getElements(Stage stage, Iterable<? extends Module> modules) {
        RecordingBinder binder = new RecordingBinder(stage);
        for (Module module : modules) {
            binder.install(module);
        }
        binder.scanForAnnotatedMethods();
        for (RecordingBinder child : binder.privateBinders) {
            child.scanForAnnotatedMethods();
        }
        // Free the memory consumed by the stack trace elements cache
        StackTraceElements.clearCache();
        return Collections.unmodifiableList(binder.elements);
    }

    private static class ElementsAsModule implements Module {
        private final Iterable<? extends Element> elements;

        ElementsAsModule(Iterable<? extends Element> elements) {
            this.elements = elements;
        }

        @Override
        public void configure(Binder binder) {
            for (Element element : elements) {
                element.applyTo(binder);
            }
        }
    }

    /**
     * Returns the module composed of {@code elements}.
     */
    public static Module getModule(final Iterable<? extends Element> elements) {
        return new ElementsAsModule(elements);
    }

    @SuppressWarnings("unchecked")
    static <T> BindingTargetVisitor<T, T> getInstanceVisitor() {
        return (BindingTargetVisitor<T, T>) GET_INSTANCE_VISITOR;
    }

    private static class ModuleInfo {
        private final Binder binder;
        private final ModuleSource moduleSource;
        private final boolean skipScanning;

        private ModuleInfo(Binder binder, ModuleSource moduleSource, boolean skipScanning) {
            this.binder = binder;
            this.moduleSource = moduleSource;
            this.skipScanning = skipScanning;
        }
    }

    private static class RecordingBinder implements Binder, PrivateBinder {
        private final Stage stage;
        private final Map<Module, ModuleInfo> modules;
        private final List<Element> elements;
        private final Object source;
        /** The current modules stack */
        private ModuleSource moduleSource = null;
        private final SourceProvider sourceProvider;
        private final Set<ModuleAnnotatedMethodScanner> scanners;

        /** The binder where exposed bindings will be created */
        private final RecordingBinder parent;
        private final PrivateElementsImpl privateElements;

        /** All children private binders, so we can scan through them. */
        private final List<RecordingBinder> privateBinders;

        private RecordingBinder(Stage stage) {
            this.stage = stage;
            this.modules = Maps.newLinkedHashMap();
            this.scanners = Sets.newLinkedHashSet();
            this.elements = Lists.newArrayList();
            this.source = null;
            this.sourceProvider = SourceProvider.DEFAULT_INSTANCE.plusSkippedClasses(Elements.class, RecordingBinder.class, AbstractModule.class, ConstantBindingBuilderImpl.class, AbstractBindingBuilder.class, BindingBuilder.class);
            this.parent = null;
            this.privateElements = null;
            this.privateBinders = Lists.newArrayList();
        }

        /** Creates a recording binder that's backed by {@code prototype}. */
        private RecordingBinder(RecordingBinder prototype, Object source, SourceProvider sourceProvider) {
            checkArgument(source == null ^ sourceProvider == null);

            this.stage = prototype.stage;
            this.modules = prototype.modules;
            this.elements = prototype.elements;
            this.scanners = prototype.scanners;
            this.source = source;
            this.moduleSource = prototype.moduleSource;
            this.sourceProvider = sourceProvider;
            this.parent = prototype.parent;
            this.privateElements = prototype.privateElements;
            this.privateBinders = prototype.privateBinders;
        }

        /** Creates a private recording binder. */
        private RecordingBinder(RecordingBinder parent, PrivateElementsImpl privateElements) {
            this.stage = parent.stage;
            this.modules = Maps.newLinkedHashMap();
            this.scanners = Sets.newLinkedHashSet(parent.scanners);
            this.elements = privateElements.getElementsMutable();
            this.source = parent.source;
            this.moduleSource = parent.moduleSource;
            this.sourceProvider = parent.sourceProvider;
            this.parent = parent;
            this.privateElements = privateElements;
            this.privateBinders = parent.privateBinders;
        }

        /*if[AOP]*/
        @Override
        public void bindInterceptor(Matcher<? super Class<?>> classMatcher, Matcher<? super Method> methodMatcher, org.aopalliance.intercept.MethodInterceptor... interceptors) {
            elements.add(new InterceptorBinding(getElementSource(), classMatcher, methodMatcher, interceptors));
        }
        /*end[AOP]*/

        @Override
        public void bindScope(Class<? extends Annotation> annotationType, Scope scope) {
            elements.add(new ScopeBinding(getElementSource(), annotationType, scope));
        }

        @Override
        @SuppressWarnings("unchecked") // it is safe to use the type literal for the raw type
        public void requestInjection(Object instance) {
            requestInjection((TypeLiteral<Object>) TypeLiteral.get(instance.getClass()), instance);
        }

        @Override
        public <T> void requestInjection(TypeLiteral<T> type, T instance) {
            elements.add(new InjectionRequest<T>(getElementSource(), MoreTypes.canonicalizeForKey(type), instance));
        }

        @Override
        public <T> MembersInjector<T> getMembersInjector(final TypeLiteral<T> typeLiteral) {
            final MembersInjectorLookup<T> element = new MembersInjectorLookup<T>(getElementSource(), MoreTypes.canonicalizeForKey(typeLiteral));
            elements.add(element);
            return element.getMembersInjector();
        }

        public <T> MembersInjector<T> getMembersInjector(Class<T> type) {
            return getMembersInjector(TypeLiteral.get(type));
        }

        public void bindListener(Matcher<? super TypeLiteral<?>> typeMatcher, TypeListener listener) {
            elements.add(new TypeListenerBinding(getElementSource(), listener, typeMatcher));
        }

        public void bindListener(Matcher<? super Binding<?>> bindingMatcher, ProvisionListener... listeners) {
            elements.add(new ProvisionListenerBinding(getElementSource(), bindingMatcher, listeners));
        }

        public void requestStaticInjection(Class<?>... types) {
            for (Class<?> type : types) {
                elements.add(new StaticInjectionRequest(getElementSource(), type));
            }
        }

        /**
         * Applies all scanners to the modules we've installed. We skip certain
         * PrivateModules because store them in more than one Modules map and only
         * want to process them through one of the maps.  (They're stored in both
         * maps to prevent a module from being installed more than once.)
         */
        void scanForAnnotatedMethods() {
            for (ModuleAnnotatedMethodScanner scanner : scanners) {
                // Note: we must iterate over a copy of the modules because calling install(..)
                // will mutate modules, otherwise causing a ConcurrentModificationException.
                for (Map.Entry<Module, ModuleInfo> entry : Maps.newLinkedHashMap(modules).entrySet()) {
                    Module module = entry.getKey();
                    ModuleInfo info = entry.getValue();
                    if (info.skipScanning) {
                        continue;
                    }
                    moduleSource = entry.getValue().moduleSource;
                    try {
                        info.binder.install(ProviderMethodsModule.forModule(module, scanner));
                    } catch (RuntimeException e) {
                        Collection<Message> messages = Errors.getMessagesFromThrowable(e);
                        if (!messages.isEmpty()) {
                            elements.addAll(messages);
                        } else {
                            addError(e);
                        }
                    }
                }
            }
            moduleSource = null;
        }

        public void install(Module module) {
            if (!modules.containsKey(module)) {
                RecordingBinder binder = this;
                boolean unwrapModuleSource = false;
                // Update the module source for the new module
                if (module instanceof ProviderMethodsModule) {
                    // There are two reason's we'd want to get the module source in a ProviderMethodsModule.
                    // ModuleAnnotatedMethodScanner lets users scan their own modules for @Provides-like
                    // bindings.  If they install the module at a top-level, then moduleSource can be null.
                    // Also, if they pass something other than 'this' to it, we'd have the wrong source.
                    Object delegate = ((ProviderMethodsModule) module).getDelegateModule();
                    if (moduleSource == null || !moduleSource.getModuleClassName().equals(delegate.getClass().getName())) {
                        moduleSource = getModuleSource(delegate);
                        unwrapModuleSource = true;
                    }
                } else {
                    moduleSource = getModuleSource(module);
                    unwrapModuleSource = true;
                }
                boolean skipScanning = false;
                if (module instanceof PrivateModule) {
                    binder = (RecordingBinder) binder.newPrivateBinder();
                    // Store the module in the private binder too so we scan for it.
                    binder.modules.put(module, new ModuleInfo(binder, moduleSource, false));
                    skipScanning = true; // don't scan this module in the parent's module set.
                }
                // Always store this in the parent binder (even if it was a private module)
                // so that we know not to process it again, and so that scanners inherit down.
                modules.put(module, new ModuleInfo(binder, moduleSource, skipScanning));
                try {
                    module.configure(binder);
                } catch (RuntimeException e) {
                    Collection<Message> messages = Errors.getMessagesFromThrowable(e);
                    if (!messages.isEmpty()) {
                        elements.addAll(messages);
                    } else {
                        addError(e);
                    }
                }
                binder.install(ProviderMethodsModule.forModule(module));
                // We are done with this module, so undo module source change
                if (unwrapModuleSource) {
                    moduleSource = moduleSource.getParent();
                }
            }
        }

        public Stage currentStage() {
            return stage;
        }

        public void addError(String message, Object... arguments) {
            elements.add(new Message(getElementSource(), Errors.format(message, arguments)));
        }

        public void addError(Throwable t) {
            String message = "An exception was caught and reported. Message: " + t.getMessage();
            elements.add(new Message(ImmutableList.of((Object) getElementSource()), message, t));
        }

        public void addError(Message message) {
            elements.add(message);
        }

        public <T> AnnotatedBindingBuilder<T> bind(Key<T> key) {
            BindingBuilder<T> builder = new BindingBuilder<T>(this, elements, getElementSource(), MoreTypes.canonicalizeKey(key));
            return builder;
        }

        public <T> AnnotatedBindingBuilder<T> bind(TypeLiteral<T> typeLiteral) {
            return bind(Key.get(typeLiteral));
        }

        public <T> AnnotatedBindingBuilder<T> bind(Class<T> type) {
            return bind(Key.get(type));
        }

        public AnnotatedConstantBindingBuilder bindConstant() {
            return new ConstantBindingBuilderImpl<Void>(this, elements, getElementSource());
        }

        public <T> Provider<T> getProvider(final Key<T> key) {
            return getProvider(Dependency.get(key));
        }

        public <T> Provider<T> getProvider(final Dependency<T> dependency) {
            final ProviderLookup<T> element = new ProviderLookup<T>(getElementSource(), dependency);
            elements.add(element);
            return element.getProvider();
        }

        public <T> Provider<T> getProvider(Class<T> type) {
            return getProvider(Key.get(type));
        }

        public void convertToTypes(Matcher<? super TypeLiteral<?>> typeMatcher, TypeConverter converter) {
            elements.add(new TypeConverterBinding(getElementSource(), typeMatcher, converter));
        }

        public RecordingBinder withSource(final Object source) {
            return source == this.source ? this : new RecordingBinder(this, source, null);
        }

        public RecordingBinder skipSources(Class... classesToSkip) {
            // if a source is specified explicitly, we don't need to skip sources
            if (source != null) {
                return this;
            }

            SourceProvider newSourceProvider = sourceProvider.plusSkippedClasses(classesToSkip);
            return new RecordingBinder(this, null, newSourceProvider);
        }

        @Override
        public PrivateBinder newPrivateBinder() {
            PrivateElementsImpl privateElements = new PrivateElementsImpl(getElementSource());
            RecordingBinder binder = new RecordingBinder(this, privateElements);
            privateBinders.add(binder);
            elements.add(privateElements);
            return binder;
        }

        @Override
        public void disableCircularProxies() {
            elements.add(new DisableCircularProxiesOption(getElementSource()));
        }

        @Override
        public void requireExplicitBindings() {
            elements.add(new RequireExplicitBindingsOption(getElementSource()));
        }

        @Override
        public void requireAtInjectOnConstructors() {
            elements.add(new RequireAtInjectOnConstructorsOption(getElementSource()));
        }

        @Override
        public void requireExactBindingAnnotations() {
            elements.add(new RequireExactBindingAnnotationsOption(getElementSource()));
        }

        @Override
        public void scanModulesForAnnotatedMethods(ModuleAnnotatedMethodScanner scanner) {
            scanners.add(scanner);
            elements.add(new ModuleAnnotatedMethodScannerBinding(getElementSource(), scanner));
        }

        public void expose(Key<?> key) {
            exposeInternal(key);
        }

        @Override
        public AnnotatedElementBuilder expose(Class<?> type) {
            return exposeInternal(Key.get(type));
        }

        @Override
        public AnnotatedElementBuilder expose(TypeLiteral<?> type) {
            return exposeInternal(Key.get(type));
        }

        private <T> AnnotatedElementBuilder exposeInternal(Key<T> key) {
            if (privateElements == null) {
                addError("Cannot expose %s on a standard binder. " + "Exposed bindings are only applicable to private binders.", key);
                return new AnnotatedElementBuilder() {
                    @Override
                    public void annotatedWith(Class<? extends Annotation> annotationType) {
                    }

                    @Override
                    public void annotatedWith(Annotation annotation) {
                    }
                };
            }

            ExposureBuilder<T> builder = new ExposureBuilder<T>(this, getElementSource(), MoreTypes.canonicalizeKey(key));
            privateElements.addExposureBuilder(builder);
            return builder;
        }

        private ModuleSource getModuleSource(Object module) {
            StackTraceElement[] partialCallStack;
            if (getIncludeStackTraceOption() == IncludeStackTraceOption.COMPLETE) {
                partialCallStack = getPartialCallStack(new Throwable().getStackTrace());
            } else {
                partialCallStack = new StackTraceElement[0];
            }
            if (moduleSource == null) {
                return new ModuleSource(module, partialCallStack);
            }
            return moduleSource.createChild(module, partialCallStack);
        }

        private ElementSource getElementSource() {
            // Full call stack
            StackTraceElement[] callStack = null;
            // The call stack starts from current top module configure and ends at this method caller
            StackTraceElement[] partialCallStack = new StackTraceElement[0];
            // The element original source
            ElementSource originalSource = null;
            // The element declaring source
            Object declaringSource = source;
            if (declaringSource instanceof ElementSource) {
                originalSource = (ElementSource) declaringSource;
                declaringSource = originalSource.getDeclaringSource();
            }
            IncludeStackTraceOption stackTraceOption = getIncludeStackTraceOption();
            if (stackTraceOption == IncludeStackTraceOption.COMPLETE || (stackTraceOption == IncludeStackTraceOption.ONLY_FOR_DECLARING_SOURCE && declaringSource == null)) {
                callStack = new Throwable().getStackTrace();
            }
            if (stackTraceOption == IncludeStackTraceOption.COMPLETE) {
                partialCallStack = getPartialCallStack(callStack);
            }
            if (declaringSource == null) {
                // So 'source' and 'originalSource' are null otherwise declaringSource has some value
                if (stackTraceOption == IncludeStackTraceOption.COMPLETE || stackTraceOption == IncludeStackTraceOption.ONLY_FOR_DECLARING_SOURCE) {
                    // With the above conditions and assignments 'callStack' is non-null
                    declaringSource = sourceProvider.get(callStack);
                } else { // or if (stackTraceOption == IncludeStackTraceOptions.OFF)
                    // As neither 'declaring source' nor 'call stack' is available use 'module source'
                    declaringSource = sourceProvider.getFromClassNames(moduleSource.getModuleClassNames());
                }
            }
            // Build the binding call stack
            return new ElementSource(originalSource, declaringSource, moduleSource, partialCallStack);
        }

        /**
         * Removes the {@link #moduleSource} call stack from the beginning of current call stack. It
         * also removes the last two elements in order to make {@link #install(Module)} the last call
         * in the call stack.
         */
        private StackTraceElement[] getPartialCallStack(StackTraceElement[] callStack) {
            int toSkip = 0;
            if (moduleSource != null) {
                toSkip = moduleSource.getStackTraceSize();
            }
            // -1 for skipping 'getModuleSource' and 'getElementSource' calls
            int chunkSize = callStack.length - toSkip - 1;

            StackTraceElement[] partialCallStack = new StackTraceElement[chunkSize];
            System.arraycopy(callStack, 1, partialCallStack, 0, chunkSize);
            return partialCallStack;
        }

        @Override
        public String toString() {
            return "Binder";
        }
    }
}
